﻿using System;
using System.Diagnostics;
using System.Collections.Generic;

using Microsoft.Xna.Framework;

namespace Nine
{
	[DebuggerNonUserCode]
	public sealed class SceneManager : DrawableGameComponent, ISceneManager
	{
		#region IServiceProvider<Scene> Members
		private Dictionary<Type, Scene> Scenes;

		public TScene Get<TScene>()
			where TScene : Scene
		{
            try
            { return (TScene)this.Scenes[typeof(TScene)]; }
            catch (KeyNotFoundException ex)
            { throw new KeyNotFoundException("Unknown Scene of type " + typeof(TScene).Name + ".", ex); }
		}

		public bool Contains<TScene>()
			where TScene : Scene
		{
			return this.Scenes.ContainsKey(typeof(TScene));
		}

		public void Set<TScene>(Scene scene)
			where TScene : Scene
		{
            Type type = typeof(TScene);

			if (scene == null)
				throw new ArgumentNullException("scene");
			if (scene.Manager != null)
				throw new ArgumentException("Scene already belongs to a SceneManager.", "scene");
			if (!type.IsAssignableFrom(scene.GetType()))
				throw new InvalidCastException("Cannot convert a scene of type " + scene.GetType().Name +
											   " to " + type.Name + ".");

			this.Scenes[type] = scene;
			scene.Manager = this;
		}

		public bool Remove<TScene>()
			where TScene : Scene
		{
			Type type = typeof(TScene);

			if (this.Scenes.ContainsKey(type))
				this.Scenes[type].Manager = null;
			return this.Scenes.Remove(type);
		}
		#endregion

		#region ISceneManager Members
		private List<Scene> Active;
		private List<Scene> Pushed;

        private int Poped = 0;

		private int FirstUpdate = 0;
		private int FirstDraw = 0;

		public void Pop()
		{
            if (this.Pushed.Count > 0)
                this.Pushed.RemoveAt(this.Pushed.Count - 1);
            else if (this.Active.Count > this.Poped)
                this.Poped++;
            else
                throw new InvalidOperationException("No active Scene to Pop.");
		}

        public void Push<TScene>()
			where TScene : Scene
		{
			Scene scene = this.Get<TScene>();

			if (this.Pushed.Contains(scene) || this.Active.Contains(scene))
				throw new InvalidOperationException(typeof(TScene).Name + " already pushed.");

			this.Pushed.Add(scene);
		}

		private void First()
		{
			for (this.FirstUpdate = this.Active.Count - 1;
				 this.FirstUpdate >= 0 && (this.Active[this.FirstUpdate].Filter & SceneFilter.Update) != 0;
				 this.FirstUpdate--) ;

			for (this.FirstDraw = this.Active.Count - 1;
				 this.FirstDraw >= 0 && (this.Active[this.FirstDraw].Filter & SceneFilter.Draw) != 0;
				 this.FirstDraw--) ;
		}
		#endregion

		public override void Update(GameTime Gt)
		{
            // Update active Scenes
			for (int i = this.FirstUpdate; i < this.Active.Count - 1; i++)
				this.Active[i].Update(Gt);

            // Pop old Scenes
            if (this.Poped > 0)
            {
                for (; this.Poped > 0; this.Poped--)
                {
                    this.Active[this.Active.Count - 1].OnPoped();
                    this.Active.RemoveAt(this.Active.Count - 1);
                }
                if (this.Pushed.Count == 0)
                    this.First();
            }
            // Push new Scenes
			if (this.Pushed.Count > 0)
			{
                for (int i = 0; i < this.Pushed.Count; i++)
                {
                    this.Active.Add(this.Pushed[i]);
                    this.Pushed[i].OnPushed();
                }
				this.Pushed.Clear();
				this.First();
			}
		}

		public override void Draw(GameTime Gt)
		{
			for (int i = this.FirstDraw; i < this.Active.Count - 1; i++)
				this.Active[i].Draw(Gt);
		}

		public SceneManager(Game game)
			: this(4, game) { }

		public SceneManager(int capacity, Game game)
			: base(game)
		{
			Active = new List<Scene>(capacity);
			Pushed = new List<Scene>(capacity);
            Scenes = new Dictionary<Type, Scene>(capacity);

            base.Game.Services.AddService(typeof(SceneManager), this);
		}
	}
}
